#include <stdio.h>
#include <stdint.h>

#include "unet/misc.h"
#include "unet/udp.h"

#include "unet/list.h"

#include <arpa/inet.h>

static uint8_t init_unet_udp = 0;

void 
unet_udp_init()
{
	//inicialitzam la llista de connexions
	INIT_LIST_HEAD (&conn_list);
	pthread_mutex_init(&conn_list_mutex,0);

}

void
unet_udp_send (unet_udp_connection_t state, const void *buf, size_t n)
{
  
  //l'aplicació vol enviar les dades de buf, creem un paquet udp amb les dades de buf i l'enviem cap a IP
	struct unet_udp_packet * new_packet=NULL;
	struct udphdr * header = (struct udphdr *) new_packet;
        new_packet = malloc(sizeof(*new_packet));
	memset(new_packet, 0, sizeof(*new_packet));
	
	uint16_t packet_len = sizeof(struct udphdr) + n;
	header->source = 0; //es un camp opcional, sino es fa servir es posa a 0
	header->dest = htons(state->remote_port);
	header->len = packet_len;

	memcpy(new_packet->payload, buf, n);
	
	if (unet_transport_send_packet(UNET_UDP, state->local_addr, new_packet, packet_len) == -1)
	{
		printf("UDP: Error from IP when sending packet");
	}

	free(new_packet);
	
	printf ("sending %lu bytes via UDP\n", (unsigned long) n);
}

size_t
unet_udp_recv (unet_udp_connection_t state, void *buf, size_t n)
{
    size_t read_data = 0; //Nombre de bytes que hem llegit

   //state:
  //       ip d'escolta
  //       
  //Esperar a que ens arribi un paquet
  //Una vegada arriba el paquet:
  //Omplir buf, amb el payload que t'ha arribat
  //Si error retornar < 0, sino retornar la mida del payload UDP que t'arriba

	//crear una connexió
	unet_udp_connection_t new_con;
	new_con = malloc(sizeof(*new_con));
	memset(new_con, 0, sizeof(*new_con));

	new_con->local_addr = state->local_addr;
	new_con->remote_port = state->remote_port;
	sem_init(&(new_con->sem_recv),1,-1);
	INIT_LIST_HEAD (&(new_con->pack_list));

	struct unet_udp_packet * pack;
	struct list_head * l;

	//fins que no ens arribin els n bytes, esperem
	while(read_data < n)
	{  	
		sem_wait(&(new_con->sem_recv));
   		//tenim un paquet!

		pthread_mutex_lock(&(new_con->mutex));
		l = list_first(&(new_con->pack_list));
		pack = list_head_to_udp_packet(l);
		list_del(l);
		pthread_mutex_unlock(&(new_con->mutex));
		
		uint8_t data_len = sizeof(*pack) - sizeof(struct udphdr);		
		memcpy(buf, pack->payload, data_len);

		read_data += data_len;
		free(pack);
	}
	return read_data;
}

void
unet_udp_incoming (struct in_addr local_ip, const void *buf, size_t n)
{
  //Arriba un paquet UDP (buf) des de IP que té una adreça IP i un port desti
        struct unet_udp_packet * new_packet=NULL;
	struct udphdr * header = (struct udphdr *) new_packet;
        new_packet = malloc(sizeof(*new_packet));
	memset(new_packet, 0, sizeof(*new_packet));

	int header_len = sizeof(struct udphdr);
	
	memcpy(&(new_packet->header), header, sizeof(struct udphdr));
	memcpy(new_packet->payload, header + header_len, n - header_len);

	       	//buscar la connexio a la llista de connexions i afegir el paquet a la llista
		struct list_head * pos;			
		unet_udp_connection_t conn_act;
		uint8_t found = 0;

		pthread_mutex_lock(&conn_list_mutex);
		list_for_each(pos, &conn_list)
		{
			conn_act = (unet_udp_connection_t) list_head_to_udp_connection(pos);
			if((conn_act->remote_port == new_packet->header.dest) && (inet_ntoa(conn_act->local_addr) == inet_ntoa(local_ip)))
			{
				//afegir a la llista de packets de conn_act el nou packet
				found = 1;
			
				pthread_mutex_lock(&(conn_act->mutex));
				list_add_tail(&(new_packet->packet), &(conn_act->pack_list));
				pthread_mutex_unlock(&(conn_act->mutex));

				sem_post(&(conn_act->sem_recv));				
			}
			
		}  
		pthread_mutex_unlock(&conn_list_mutex); 

		if (found != 1)
		{
			free(new_packet);			
			printf("UDP: no connection");
		} 
		
        

}

unet_udp_connection_t
unet_udp_connect (struct in_addr dest_addr, uint16_t src_port, uint16_t dst_port)
{
	//si encara no s'ha inicialitzat udp, inicialitzem i creem una nova connexio, sino nomes creem la connexio	
	if(init_unet_udp == 0)
	{
		unet_udp_init();
		init_unet_udp = 1;		
	}
		unet_udp_connection_t new_conn;		
		new_conn = malloc(sizeof(*new_conn));
		memset(new_conn, 0, sizeof(*new_conn));
		
		new_conn->local_addr = dest_addr;
		new_conn->remote_port = htons(dst_port);
		INIT_LIST_HEAD (&(new_conn->pack_list));
		sem_init(&(new_conn->sem_recv),1,-1);
		pthread_mutex_init(&(new_conn->mutex),0);

		pthread_mutex_lock(&conn_list_mutex);
		list_add_tail(&(new_conn->connection), &conn_list);
		pthread_mutex_unlock(&conn_list_mutex);
			
 
  printf ("connecting with %s:%u via UDP\n", inet_ntoa (dest_addr), dst_port);
  return new_conn;
}

void
unet_udp_conn_close (unet_udp_connection_t state)
{
	//eliminem la connexio de la llista
	pthread_mutex_lock(&conn_list_mutex);	
	list_del(&(state->connection));
	pthread_mutex_unlock(&conn_list_mutex);		

	struct unet_udp_packet * pack;	

	pthread_mutex_lock(&(state->mutex));
	while(!list_empty(&(state->pack_list)))
	{
		struct list_head * l;
		l = list_first(&(state->pack_list));
		list_del(l);
		pack = list_head_to_udp_packet(l);
		free(pack);	
	}
	pthread_mutex_unlock(&(state->mutex));

	free(state);  

	printf ("closing connection\n");

}


unet_udp_connection_t *
list_head_to_udp_connection(struct list_head *l) {
    unet_udp_connection_t dummy = (unet_udp_connection_t) 0;

    struct list_head *lh = &(dummy->connection);

    return (unet_udp_connection_t *) (((char *) (l) - (unsigned long) lh));
}

struct unet_udp_packet *
list_head_to_udp_packet(struct list_head *l) {
    return list_entry(l, struct unet_udp_packet, packet);
}


